为什么 A 合入 master 无冲突,B 同步 master 却冲突:从 merge-base 理解三方合并
feature-a 合入 master 没有冲突,不代表基于 A 开发的 feature-b 再合并 master 时也不会冲突。
两次 merge 使用的共同祖先不同,比较的提交范围也不同。第一次比较的是 A 与 master 的并行修改,第二次比较的是 B 与 master 的并行修改。只要 B 和 master 修改了相同代码区域,第二次合并就可能产生冲突。
一、还原提交关系
假设存在三个分支:
master:主分支。feature-a:从 master 创建的第一阶段功能。feature-b:从 feature-a 创建的第二阶段功能。
在 A 合入 master 前,历史如下:
B1---B2 feature-b
/
A1---A2 feature-a
/
O----+
\
M1---M2 master其中:
A1、A2是 feature-a 的修改。B1、B2是 feature-b 在 A2 之上的修改。M1、M2是其他开发者在 master 上提交的修改。- feature-b 开发期间没有同步
M1、M2。
在 master 上执行:
git switch master
git merge feature-a如果合并成功,历史变为:
B1---B2 feature-b
/
A1---A2 feature-a
/ \
O----+ MA master
\ /
M1---M2MA 是 merge commit,它有两个父提交:
A2
M2此时 master 同时包含 A1、A2、M1、M2。
二、第一次合并为什么没有冲突
在 master 上执行:
git merge feature-aGit 会进行三方合并,参与比较的是:
共同祖先:O
ours: master,也就是 M1、M2 一侧
theirs: feature-a,也就是 A1、A2 一侧可以把它理解为比较两组差异:
O -> M2
O -> A2如果 M1、M2 和 A1、A2 没有对相同代码区域做不兼容修改,Git 就能自动合并。
因此,第一次无冲突只能证明:
A 的修改与当时 master 的修改可以自动合并它不能证明 feature-b 也能与 master 自动合并。
三、第二次合并的基准已经改变
现在切换到 feature-b,同步最新 master:
git switch feature-b
git merge masterGit 会重新计算 feature-b 和 master 的共同祖先。
由于 feature-b 基于 A2 开发,而 A2 已经通过 merge commit MA 进入 master,因此新的共同祖先通常是:
A2这一次参与三方合并的是:
共同祖先:A2
ours: feature-b,也就是 B1、B2
theirs: master 在 A2 之后引入的修改Git 比较的差异变成:
A2 -> B2
A2 -> MA两次 merge 的比较范围完全不同:
| 操作 | 当前分支 | merge-base | ours 的修改 | theirs 的修改 |
|---|---|---|---|---|
| master 合并 feature-a | master | O | M1、M2 | A1、A2 |
| feature-b 合并 master | feature-b | A2 | B1、B2 | master 相对 A2 的修改 |
第一次比较的是:
A 与 master第二次比较的是:
B 与 master如果 B1、B2 修改了 M1、M2 也修改过的代码区域,第二次合并就可能产生冲突。
四、Git 如何判断冲突
Git merge 不是简单比较两个分支的最终文件,而是进行三方合并:
共同祖先
当前分支 ours
被合入分支 theirs对于每个文件,Git会计算:
merge-base -> ours
merge-base -> theirs如果两边修改互不干扰,Git可以自动组合;如果两边对相同区域进行了无法自动协调的修改,就会要求人工处理。
冲突不只包括“同一行修改不同内容”,还可能包括:
一边修改文件,另一边删除文件
两边新增同名文件
文件重命名与内容修改冲突
目录与文件名称冲突
多个相邻修改块无法安全拼接还需要注意:没有文本冲突,不代表没有业务冲突。Git 只能判断文件内容能否合并,不能判断两个修改组合后业务逻辑是否正确。
五、如何确认实际的 merge-base
查看 feature-b 与 master 的共同祖先:
git merge-base feature-b master查看完整提交图:
git log --graph --oneline --decorate --all查看共同祖先到 feature-b 的修改:
BASE=$(git merge-base feature-b master)
git diff "$BASE..feature-b"$base = git merge-base feature-b master
git diff "$base..feature-b"查看共同祖先到 master 的修改:
BASE=$(git merge-base feature-b master)
git diff "$BASE..master"$base = git merge-base feature-b master
git diff "$base..master"如果两个 diff 修改了相同文件和相邻代码区域,就能解释冲突来源。
还可以提前模拟合并结果,但不生成提交:
git switch feature-b
git merge --no-commit --no-ff master检查完成后取消:
git merge --abort执行前应确保工作区干净。
六、使用 Merge 同步 master
对于已推送或多人协作的 feature-b,merge 通常更稳妥:
git switch feature-b
git status
git fetch origin
git merge origin/master发生冲突后:
git status逐个处理冲突文件,再执行:
git add <resolved-files>
git commit完成后运行测试:
npm test根据项目实际情况,也可能需要执行:
npm install
npm run build
npm run lint确认无误后推送:
git push origin feature-bmerge 会保留 feature-b 原提交,并增加一个 merge commit:
B1---B2------MB feature-b
/ /
A1---A2----------MA master
/ /
O----+-----------------+
\
M1---M2如果 master 在此后没有新增提交,将 feature-b 合回 master 时可能快进;是否实际 fast-forward,还取决于平台合并策略和分支保护规则。
七、使用 Rebase 同步 master
如果 feature-b 只有自己使用,并且团队允许重写历史,可以选择:
git switch feature-b
git status
git fetch origin
git branch backup/feature-b-before-rebase
git rebase origin/master执行前:
B1---B2 feature-b
/
A1---A2
\
MA master
/
M1---M2执行后:
A1---A2---MA---B1'---B2' feature-b
↑
masterB1'、B2' 是重新生成的提交。它们的代码修改可能与 B1、B2 相同,但父提交发生了变化,因此哈希不同。
处理 Rebase 冲突
rebase 会逐个重放提交,冲突也会按提交分批出现。
解决当前提交的冲突:
git status
git add <resolved-files>
git rebase --continue取消整个 rebase:
git rebase --abort跳过当前提交:
git rebase --skip--skip 会丢弃当前正在重放的整个提交,除非确认修改已经存在或确实不再需要,否则不要使用。
Rebase 后如何推送
如果 feature-b 从未推送过,可以正常推送:
git push -u origin feature-b如果远程已经存在 rebase 前的 feature-b,需要改写远程历史:
git push --force-with-lease origin feature-b--force-with-lease 会检查远程分支是否仍指向本地预期的提交。如果其他人已经更新远程分支,推送会被拒绝。
它比下面的命令安全:
git push --force但 --force-with-lease 仍然会改写远程历史,不能把它理解成普通 push。
八、Merge 与 Rebase 如何选择
| 对比项 | git merge origin/master | git rebase origin/master |
|---|---|---|
| 原提交哈希 | 保留 | 重新生成 |
| 历史形态 | 保留分叉和 merge commit | 线性历史 |
| 冲突处理 | 通常集中在一次 merge 中 | 按提交逐个处理 |
| 共享分支 | 适合 | 通常不适合 |
| 是否需要强推 | 不需要 | 已推送时通常需要 |
| 并行开发过程 | 保留 | 被重新组织 |
| 适合场景 | 团队协作分支 | 个人未共享分支 |
可以使用以下原则:
个人本地分支、尚未共享:可以 rebase
已经推送但确认只有自己使用:协调后可 rebase
多人共同维护的分支:优先 merge
release、master 等共享分支:不要随意 rebase
合入主干:遵循 PR/MR 和仓库保护策略九、为什么不建议 Rebase 共享分支
假设同事已经基于旧的 B1、B2 开发:
B1---B2---C1 colleague此时将 feature-b rebase 并强推:
MA---B1'---B2' origin/feature-bGit 不会认为 B1 和 B1' 是同一个提交。其他开发者再同步时,可能出现:
本地与远程分叉
重复提交
额外冲突
需要重新 rebase 或 cherry-pickrebase 前至少确认:
分支是否已经推送
是否有人基于该分支继续开发
仓库是否允许 force push
CI、PR 或发布流程是否引用旧提交任一条件不明确,都应优先选择 merge。
十、降低长期分支的冲突成本
1. 缩短同步间隔
不要等到功能全部完成后才同步 master。可以定期执行:
git fetch origin
git merge origin/master个人分支也可以根据团队约定执行:
git fetch origin
git rebase origin/master频繁同步不会消除所有逻辑冲突,但能缩小单次冲突范围,并让开发者在仍熟悉上下文时处理问题。
2. 启用 rerere
Git 可以记录人工解决冲突的方式:
git config --global rerere.enabled true当后续遇到结构相同的冲突时,Git 会尝试复用之前的解决结果。
查看记录:
git rerere status
git rerere diffrerere 只复用历史解决方案,仍然需要运行测试确认结果符合当前业务。
3. 明确团队历史策略
建议在 CONTRIBUTING.md 中约定:
哪些分支允许 rebase
哪些分支禁止 force push
功能分支如何同步主干
PR 使用 merge、squash 还是 rebase merge
冲突由谁解决工具配置无法替代团队约定。
十一、需要特别区分的合并方式
前面的分析建立在 feature-a 通过普通 merge 进入 master 的前提下。
如果平台使用的是 squash merge:
A1、A2 被压缩成一个新提交 S那么 A2 通常不会成为 master 的祖先。此时 feature-b 与 master 的 merge-base 可能仍然是更早的提交,Git 可能再次看到 A 的修改,从而产生重复提交或额外冲突。
同样,以下操作也会改变祖先关系:
rebase
cherry-pick
reset
force push
删除并重建分支因此,分析冲突前不要只看提交标题,应检查提交哈希、父节点和 merge-base:
git log --graph --oneline --decorate --all
git merge-base feature-b master
git show -s --format=%P <commit>十二、关于 Pull --rebase
明确执行:
git pull --rebase大致相当于:
git fetch
git rebase 对应的远程跟踪分支可以配置 pull 默认使用 rebase:
git config --global pull.rebase true关闭默认 rebase:
git config --global pull.rebase false需要注意,普通 git pull 的具体行为取决于 Git 版本、分支关系以及 pull.rebase、pull.ff 等配置:
- 可以直接 fast-forward;
- 可以使用 merge;
- 可以使用 rebase;
- 分支分叉且未配置策略时,也可能要求用户明确选择。
因此,不应简单认为所有 git pull 都会产生 merge commit。
十三、决策清单
遇到类似情况时,可以按以下顺序判断:
1. 当前分支和目标分支的 merge-base 是什么?
2. 两边分别修改了 merge-base 之后的哪些内容?
3. feature-a 是普通 merge、squash merge,还是 rebase merge?
4. feature-b 是否已经推送或被他人使用?
5. 团队是否允许重写远程历史?
6. 应该使用 merge 还是 rebase?
7. 冲突解决后是否完成测试和构建?总结
A 合入 master 没有冲突,B 同步 master 产生冲突,是完全正常的 Git 行为。
根本原因是两次操作使用了不同的共同祖先:
第一次比较:A 与 master 的修改
第二次比较:B 与 master 的修改第一次无冲突只能说明 A 与当时的 master 可以自动合并,不能推导出 B 与 master 也能自动合并。
处理方式取决于分支是否共享:
共享分支优先 merge
个人未共享分支可以 rebase
已推送后 rebase 需要谨慎使用 --force-with-lease真正需要掌握的不是如何消灭所有冲突,而是如何通过 merge-base 理解 Git 正在比较什么,并选择符合团队协作方式的历史策略。
